import path from 'path'

import { beforeAll, describe, expect, test, vi } from 'vitest'
import GithubSlugger from 'github-slugger'
import { decode } from 'html-entities'
import { chain, pick } from 'lodash-es'

import { loadPages } from '@/frame/lib/page-data'
import libLanguages from '@/languages/lib/languages-server'
import { liquid } from '@/content-render/index'
import patterns from '@/frame/lib/patterns'
import removeFPTFromPath from '@/versions/lib/remove-fpt-from-path'
import type { Page } from '@/types'

const languageCodes = Object.keys(libLanguages)
const slugger = new GithubSlugger()

describe('pages module', () => {
  vi.setConfig({ testTimeout: 60 * 1000 })

  let pages: Page[]

  beforeAll(async () => {
    pages = await loadPages()
  })

  describe('loadPages', () => {
    test('yields a non-empty array of Page objects', async () => {
      expect(Array.isArray(pages)).toBe(true)
      expect(pages.length).toBeGreaterThan(100)
    })

    test('every page has a `languageCode`', async () => {
      expect(pages.every((page) => languageCodes.includes(page.languageCode))).toBe(true)
    })

    test('every page has a non-empty `permalinks` array', async () => {
      const brokenPages = pages.filter(
        (page) => !Array.isArray(page.permalinks) || page.permalinks.length === 0,
      )

      const expectation = JSON.stringify(
        brokenPages.map((page) => page.fullPath),
        null,
        2,
      )
      expect(brokenPages.length, expectation).toBe(0)
    })

    test('redirect_from routes are unique across English pages', () => {
      const englishPages = chain(pages)
        .filter(['languageCode', 'en'])
        .filter('redirect_from')
        .map((page) => pick(page, ['redirect_from', 'applicableVersions', 'fullPath']))
        .value()

      // Map from redirect path to Set of file paths
      const redirectToFiles = new Map<string, Set<string>>()
      const versionedRedirects: Array<{ path: string; file: string }> = []

      // Page objects have dynamic properties from chain/lodash that aren't fully typed
      for (const page of englishPages) {
        const pageObj = page as Record<string, unknown>
        for (const redirect of pageObj.redirect_from as string[]) {
          for (const version of pageObj.applicableVersions as string[]) {
            const versioned = removeFPTFromPath(path.posix.join('/', version, redirect))
            versionedRedirects.push({ path: versioned, file: pageObj.fullPath as string })
            if (!redirectToFiles.has(versioned)) {
              redirectToFiles.set(versioned, new Set<string>())
            }
            redirectToFiles.get(versioned)!.add(pageObj.fullPath as string)
          }
        }
      }

      // Only consider as duplicate if more than one unique file defines the same redirect
      const duplicates = Array.from(redirectToFiles.entries())
        .filter(([, files]) => files.size > 1)
        .map(([redirectPath]) => redirectPath)

      // Build a detailed message with sources for each duplicate
      const message = `Found ${duplicates.length} duplicate redirect_from path${duplicates.length === 1 ? '' : 's'}.
        Ensure that you don't define the same path more than once in the redirect_from property in a single file and across all English files.
        You may also receive this error if you have defined the same children property more than once.\n${duplicates
          .map((dup) => {
            const files = Array.from(redirectToFiles.get(dup) || [])
            return `${dup}\n  Defined in:\n    ${files.join('\n    ')}`
          })
          .join('\n\n')}`
      expect(duplicates.length, message).toBe(0)
    })

    test('every English page has a filename that matches its slugified title or shortTitle', async () => {
      const nonMatches = pages
        .filter((page) => {
          slugger.reset()
          return (
            page.languageCode === 'en' && // only check English
            !page.relativePath.includes('index.md') && // ignore TOCs
            // Page class has dynamic frontmatter properties like 'allowTitleToDifferFromFilename' not in type definition
            !(page as Record<string, unknown>).allowTitleToDifferFromFilename && // ignore docs with override
            slugger.slug(decode(page.title)) !== path.basename(page.relativePath, '.md') &&
            slugger.slug(decode(page.shortTitle || '')) !== path.basename(page.relativePath, '.md')
          )
        })
        // make the output easier to read
        .map((page) => {
          return JSON.stringify(
            {
              file: path.basename(page.relativePath),
              title: page.title,
              path: page.fullPath,
            },
            null,
            2,
          )
        })

      const message = `
  Found ${nonMatches.length} ${
    nonMatches.length === 1 ? 'file' : 'files'
  } that do not match their slugified titles.\n
  ${nonMatches.join('\n')}\n
  To fix, run: npm run-script update-filepaths --paths [FILEPATHS]\n\n`

      expect(nonMatches.length, message).toBe(0)
    })

    test('every page has valid frontmatter', async () => {
      const frontmatterErrors = chain(pages)
        // .filter(page => page.languageCode === 'en')
        // Page class has dynamic error properties like 'frontmatterErrors' not in type definition
        .map((page) => (page as Record<string, unknown>).frontmatterErrors)
        .filter(Boolean)
        .flatten()
        .value()

      const failureMessage = `${JSON.stringify(frontmatterErrors, null, 2)}\n\n${chain(
        frontmatterErrors,
      )
        .map('filepath')
        .join('\n')
        .value()}`

      expect(frontmatterErrors.length, failureMessage).toBe(0)
    })

    test('every page has valid Liquid templating', async () => {
      const liquidErrors: Array<{ filename: string; error: string }> = []

      for (const page of pages) {
        // Page class has dynamic properties like 'raw' markdown not in type definition
        const markdown = (page as Record<string, unknown>).raw as string
        if (!patterns.hasLiquid.test(markdown)) continue
        try {
          await liquid.parse(markdown)
        } catch (error) {
          liquidErrors.push({
            filename: page.fullPath,
            error: (error as Error).message,
          })
        }
      }

      const failureMessage = JSON.stringify(liquidErrors, null, 2)
      expect(liquidErrors.length, failureMessage).toBe(0)
    })
  })
})
